今日關鍵字:無人...Redux
假如有一天,你在家煮了咖哩
不小心煮了太多
想分給對面的人
如果是以前都是平房時
直接走過去就好
如果場景變換成現代
你住在高樓裡該怎麼辦呢(用丟的?)
正常要先端著咖哩走到一樓,走到對面那棟後再爬上去...
通常做過一次之後應該不會想再做第二次了
然後更火大的是總算爬到了對面時,對面的人說:阿樓上的人剛剛分我咖哩了
這個煩惱直到有一天,管委會購買了一套無人機管理系統
透過特定的指令像無人機發送命令(可以新增)
可以讓無人機幫你運送咖哩到對面
而且由於是通過系統命令,所以下過的指令系統都會有紀錄
只要拿起家中管理系統的平板一看就知道
不會再有資訊未更新的問題
當網頁和App不大時,資料還可以一層一層傳遞
但如果結構過於複雜時,這種傳遞法不僅麻煩還容易出錯
所以這時我們需要一個掌管全局狀態的機制
而React中使用的便是Redux
yarn add redux react-redux @types/react-redux
Redux給予了以下優點
狀態的改變皆由action發出後經過reducer處理,不會發生props傳遞時不小心被改變,而導致最終難以預測狀態會如何變化的情形
已經寫好跟store互動的邏輯,不用自己撰寫
當store中的狀態真的發生改變時才會重新渲染,減少不必要的re-render
Redux的組成可分為以下幾個
創造特定的指令,用於向reducer指定進行特定的動作,可以夾帶資料(咖哩)
接收到action後進行資料的處理並更新狀態,這裡要記得reducer必須是pure function
統整reducer
劃定store的使用範圍,在Provider內部才可以取用store的資料
當然視需要甚至能做到狀態統一由redux處理,component只負責將接收的資料呈現在畫面上
不過如果只是不需共用的狀態,其實可以不必使用到redux
(當然也可以使用react內建的context API,使用方法跟解決的問題是差不多的)
可以看看Dan寫的這篇:You Might Not Need Redux
在redux中處理非同步請求不太容易,通長是交給middeware處理
這裡使用的是redux-saga
yarn add redux-saga @babel/polyfill
由於這裡比較繁瑣,但沒有篇幅讓我講太多
秉持著原來的規劃邊做邊講吧
Reducer
import * as animeActions from '../action/animeAction'
import { Anime } from '../../data/content'
const initState: { allAnime: Array<Anime> } = { allAnime:[]}
const animeReducer = (state = initState, action: animeActions.AnimeAction) => {
switch (action.type) {
case animeActions.GET_ALL_ANIMATE_SUCCESS:
return { allAnime: action.payload?.allAnime }
default:
return state
}
}
export default animeReducer
這邊先規劃store中的狀態形式以及資料的處理
action
export const GET_ALL_ANIMATE_BEGIN = 'GET_ALL_ANIMATE_BEGIN'
export const getAllAnimeBegin = () => ({
type: GET_ALL_ANIMATE_BEGIN
})
export const GET_ALL_ANIMATE_SUCCESS = 'GET_ALL_ANIMATE_SUCCESS'
export const getAllAnimateSuccess = (allAnime: Array<Anime>) => ({
type: GET_ALL_ANIMATE_SUCCESS,
payload: {
allAnime
}
})
創建兩個指令,一個是開始獲取資料,另一個是獲取資料成功後傳遞給reducerredux-saga
的角色就是component和reducer的中介,當資料獲取完成後才將指令連同資料一併送出
而不是資料還沒回來指令先送
// action
import { call, put, takeEvery } from 'redux-saga/effects'
...
function* getAnime() {
const contentPromise = new Promise<Array<Anime>>((resolve, _reject) => {
setTimeout(()=>{
const allAnime=[Anime1 , Anime2.....]
resolve(allAnime)
},0)
})
const data = yield call(() => contentPromise.then((result) => result))
// 在generator function(*)內yield可以在先停在這,配合call執行非同步操作等到拿到資料才繼續跑
yield put(getAllAnimateSuccess(data))
// 使用put觸發下一個action,將取完的資料送入reducer
}
function* animeSaga() {
yield takeEvery(GET_ALL_ANIMATE_BEGIN, getAnime)
}
// takeEvery使得接到GET_ALL_ANIMATE_BEGIN時觸發getAnime
export default animeSaga
animeSaga()
這個generator function便是在訂閱事件
saga
import { all } from 'redux-saga/effects'
import animeSaga from '../action/animeAction'
function* rootSaga() {
yield all([animeSaga()])
}
export default rootSaga
用all來整合訂閱事件的generator function
store
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { composeWithDevTools } from 'redux-devtools-extension'
import animeReducer from '../reducer/animeReducer'
import rootSaga from '../sagas'
// 創建middleware
const sagaMiddleware = createSagaMiddleware()
// 創建store,並且加入reducer及middleware
const store = createStore(
animeReducer,
composeWithDevTools(applyMiddleware(sagaMiddleware))
)
// 執行訂閱事件的saga
sagaMiddleware.run(rootSaga)
export default store
這裡偷偷多安裝了Redux DevTools Extension
yarn add redux-devtools-extension
使用方法如上,直接包在applyMiddleware()
外就可以了
作用跟redux-logger類似,同樣是debug的工具(redux-logger有點像在洗console所以我個人不怎麼喜歡用)
跟web開發不同的是,Redux DevTools Extension無法直接在瀏覽器使用
必須安裝react-native-debugger
介面長這樣
(跟瀏覽器操作感覺頗像的)
store建置好後
打開App.tsx,以Provider包住Navigation
import React from 'react'
import { Provider } from 'react-redux'
import store from './src/redux/store/store'
import Navigation from './src/components/Navigation'
const App = () => (
<Provider store={store}>
<Navigation />
</Provider>
)
export default App
這樣整個App內到處都可以自由取用store
然後HomeScreen內的資料便能從store取得
這裡需要兩個redux的hookuseDispatch
為發號指令用的hook,使用時別忘記import對應的actionuseSelector
為接收狀態用的hook
// HomeScreen.tsx
import { useDispatch, useSelector } from 'react-redux'
import { getAllAnimeBegin } from '../redux/action/animeAction'
const HomeScreen = () => {
const dispatch = useDispatch()
const allAnime = useSelector((state: RootStateType) => state.allAnime)
...
useEffect(() => {
dispatch(getAllAnimeBegin())
}, [])
// 這裡沒改變
const playListByWeek = useMemo(() => {
.....
allAnime.forEach((anime) => {
.....
}, [allAnime])
...
}
react的function component內用來模擬class component生命週期的hook
當第二參數的陣列中的依賴量發生變化時,便會執行第一參數的函式
如果第二參數為空陣列時第一參數僅會在component生成時執行一次
今天幹的事情是原本寫在component中的變數(嗯...連狀態都不是)整個挪到store裡
這樣以後別的畫面需要用到全部動畫資料的陣列時也能方便地取用
不用傳遞props傳遞到吐血
明天預計新增動畫的細節頁並建立起連結
參考: